﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using PropertyExpression.Common;

namespace PropertyExpression.ControllableQuery.Toolkit
{
    public static class QueryChecker
    {
        public static void CheckNonQueries(string connectionString, ParamTypeChecker paramTypeChecker, params Func<CheckQueryContext, INonQuery>[] queryFuncs)
        {
            foreach (var queryFunc in queryFuncs)
            {
                ExtractSqlParameter(connectionString, paramTypeChecker, queryFunc);
            }
        }

        public static void CheckQueries<T>(string connectionString, ParamTypeChecker paramTypeChecker, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, params Func<CheckQueryContext, IQuery<T>>[] queryFuncs)
        {
            foreach (var queryFunc in queryFuncs)
            {
                var extractSqlParameterResult = ExtractSqlParameter(connectionString, paramTypeChecker, queryFunc);

                var dataInfoType = typeof(T);
                var dataInfoPropertyInfos = dataInfoType.GetProperties();
                var dataInfoPropertyInfoDictionary = dataInfoPropertyInfos.ToDictionary(
                    propertyInfo => propertyInfo.Name);
                var schemaTable = connectionString.WithSqlConnection(
                    connection => connection.WithSqlCommand(
                        sqlCommand =>
                            {
                                sqlCommand.CommandText = GetCommandText(
                                    extractSqlParameterResult.SqlParamInfos.Select(
                                        sqlParamInfo => Tuple.Create(sqlParamInfo.ParamInfo.Name, sqlParamInfo.SqlType)
                                        ),
                                    extractSqlParameterResult.QueryText);

                                using (var sqlDataReader = sqlCommand.ExecuteReader(CommandBehavior.SchemaOnly))
                                {
                                    return sqlDataReader.GetSchemaTable();
                                }
                            })
                    );
                foreach (DataRow dataRow in schemaTable.Rows)
                {
                    var closuredDataRow = dataRow;
                    dataInfoPropertyInfoDictionary.GetOptionalValue(closuredDataRow.GetColumnName()).Process(
                        propertyInfo => queryResultPropertyTypeChecker(propertyInfo, closuredDataRow, () => GetQueryType(extractSqlParameterResult.Query)),
                        () =>
                            {
                                throw new QueryCheckException(
                                    string.Format(
                                        @"The query result type does not contain a definition for property '{0}'.
QueryType = '{1}'",
                                        closuredDataRow.GetColumnName(),
                                        GetQueryType(extractSqlParameterResult.Query)));
                            }
                        );
                }
                var schemaTableDictionary = schemaTable.Rows.Cast<DataRow>().ToDictionary(_ => _.GetColumnName());
                foreach (var propertyInfo in dataInfoPropertyInfos)
                {
                    if (!schemaTableDictionary.GetOptionalValue(propertyInfo.Name).HasValue())
                    {
                        throw new QueryCheckException(
                            string.Format(
                                @"The query result does not contain a column '{0}'.
QueryType = '{1}'",
                                propertyInfo.Name,
                                GetQueryType(extractSqlParameterResult.Query)));
                    }
                }
            }
        }

        private class ExtractSqlParameterResult
        {
            public readonly IEnumerable<SqlParamInfo> SqlParamInfos;
            public readonly string QueryText;
            public readonly IQuery Query;

            public ExtractSqlParameterResult(IEnumerable<SqlParamInfo> sqlParamInfos, string queryText, IQuery query)
            {
                SqlParamInfos = sqlParamInfos;
                QueryText = queryText;
                Query = query;
            }
        }

        private static ExtractSqlParameterResult ExtractSqlParameter(
            string connectionString,
            ParamTypeChecker paramTypeChecker,
            Func<CheckQueryContext, IQuery> queryFunc)
        {
            var executionResult = connectionString.WithSqlConnection(
                connection =>
                    {
                        connection.WithSqlCommand(
                            sqlCommand =>
                                {
                                    sqlCommand.CommandText = "SET SHOWPLAN_XML ON";
                                    sqlCommand.ExecuteNonQuery();
                                    return new {};
                                }
                            );

                        var paramInfoList = new List<ParamInfo>();
                        var query = queryFunc(new CheckQueryContext(
                                                  paramInfoList, paramTypeChecker,
                                                  parameterName => string.Format(@"
{0}
",
                                                                                 parameterName)));
                        var dictionary = new Dictionary<string, string>();
                        FirstPhaseExecuteQuery(connection, query.TransformText(), paramInfoList, dictionary, query);
                        var executeQueryResult = SecondPhaseExecuteQuery(connection, dictionary, queryFunc, paramTypeChecker);
                        return new
                                   {
                                       executeQueryResult,
                                       dictionary
                                   };
                    }
                );
            var xmlDocument = new XmlDocument();
            xmlDocument.LoadXml((string)executionResult.executeQueryResult.QueryResult);
            var xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable);
            xmlNamespaceManager.AddNamespace("a", "http://schemas.microsoft.com/sqlserver/2004/07/showplan");
            Console.WriteLine(executionResult.executeQueryResult.QueryText);
            var sqlParamInfoList = executionResult.executeQueryResult.ParamInfoList.Select(
                paramInfo =>
                new SqlParamInfo(
                    paramInfo,
                    executionResult.dictionary.GetOptionalValue(paramInfo.Name).Process(
                        value => value,
                        () => paramInfo.SqlTypeFunc(paramInfo, xmlDocument, xmlNamespaceManager))
                    )
                ).ToList();
            foreach (var sqlParamInfo in sqlParamInfoList)
            {
                sqlParamInfo.ParamInfo.ParamTypeChecker(
                    sqlParamInfo,
                    executionResult.executeQueryResult.QueryText,
                    GetQueryType(executionResult.executeQueryResult.Query),
                    xmlDocument, xmlNamespaceManager);
            }
            return new ExtractSqlParameterResult(
                sqlParamInfoList,
                executionResult.executeQueryResult.QueryText,
                executionResult.executeQueryResult.Query);
        }

        private static Type GetQueryType(IQuery query)
        {
            return query.GetType();
        }

        private static object FirstPhaseExecuteQuery(
            SqlConnection connection, string queryText, IEnumerable<ParamInfo> paramInfoList, Dictionary<string, string> dictionary, IQuery query)
        {
            var commandText = GetCommandText(queryText, paramInfoList, dictionary);

            return connection.WithSqlCommand<Func<object>>(
                sqlCommand =>
                    {
                        sqlCommand.CommandText = commandText;
                        try
                        {
                            var executeScalar = sqlCommand.ExecuteScalar();
                            return () => executeScalar;
                        }
                        catch (SqlException sqlException)
                        {
                            if (sqlException.Number == 1060)
                            {
                                dictionary.Add(commandText.ToLines().ToArray()[sqlException.LineNumber - 1], "bigint");
                                return () => FirstPhaseExecuteQuery(connection, queryText, paramInfoList, dictionary, query);
                            }
                            else
                            {
                                throw new QueryCheckException(sqlCommand.CommandText, sqlException, GetQueryType(query));
                            }
                        }
                    }
                )();
        }

        private class SecondPhaseExecuteQueryResult
        {
            public readonly object QueryResult;
            public readonly IQuery Query;
            public readonly string QueryText;
            public readonly List<ParamInfo> ParamInfoList;

            public SecondPhaseExecuteQueryResult(object queryResult, IQuery query, string queryText, List<ParamInfo> paramInfoList)
            {
                QueryResult = queryResult;
                Query = query;
                QueryText = queryText;
                ParamInfoList = paramInfoList;
            }
        }

        private static SecondPhaseExecuteQueryResult SecondPhaseExecuteQuery(SqlConnection connection, Dictionary<string, string> dictionary, Func<CheckQueryContext, IQuery> queryFunc, ParamTypeChecker paramTypeChecker)
        {
            var paramInfoList = new List<ParamInfo>();
            var query = queryFunc(new CheckQueryContext(paramInfoList, paramTypeChecker, parameterName => parameterName));
            var queryText = query.TransformText();
            var commandText = GetCommandText(queryText, paramInfoList, dictionary);
            return new SecondPhaseExecuteQueryResult(
                connection.WithSqlCommand(
                    sqlCommand =>
                        {
                            sqlCommand.CommandText = commandText;
                            try
                            {
                                return sqlCommand.ExecuteScalar();
                            }
                            catch (SqlException sqlException)
                            {
                                throw new QueryCheckException(sqlCommand.CommandText, sqlException, GetQueryType(query));
                            }
                        }
                    ),
                query,
                queryText,
                paramInfoList);
        }

        private static string GetCommandText(string queryText, IEnumerable<ParamInfo> paramInfoList, Dictionary<string, string> dictionary)
        {
            return GetCommandText(
                paramInfoList.Select(
                    paramInfo => Tuple.Create(
                        paramInfo.Name,
                        paramInfo.SqlDeclareInfoFunc(paramInfo).Process(
                            value => value,
                            () => dictionary
                                      .GetOptionalValue(paramInfo.Name).Process(value => value, () => "VARBINARY(MAX)")
                            ))
                    ),
                queryText);
        }

        private static string GetCommandText(IEnumerable<Tuple<string, string>> paramInfoList, string queryText)
        {
            var paramStringBuilder = new StringBuilder();
            foreach (var paramInfo in paramInfoList)
            {
                paramStringBuilder.AppendFormat("DECLARE {0} {1}", paramInfo.Item1, paramInfo.Item2);
                paramStringBuilder.AppendLine();
            }
            return string.Format(@"{0}
{1}",
                                 paramStringBuilder,
                                 queryText);
        }

        private static TResult WithSqlConnection<TResult>(this string connectionString, Func<SqlConnection, TResult> func)
        {
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();

                return func(connection);
            }
        }

        private static TResult WithSqlCommand<TResult>(this SqlConnection connection, Func<SqlCommand, TResult> func)
        {
            using (var sqlCommand = new SqlCommand())
            {
                sqlCommand.Connection = connection;

                return func(sqlCommand);
            }
        }

        public static void CheckAllQueriesInAvailableAssemblies(
            string connectionString,
            ParamTypeChecker paramTypeChecker,
            QueryResultPropertyTypeChecker queryResultPropertyTypeChecker,
            ParamCreator paramCreator,
            Func<Type, bool> queryTypepredicate,
            Func<Type, IChoiceInvocationContext,
            IEnumerable<QueryCreator>> additionalChoiceInvocationFunc)
        {
            foreach (var type in GetAllTypes())
            {
                foreach (var @interface in type.GetInterfaces())
                {
                    if (@interface.IsGenericTypeAndEquals(typeof(IQuery<>)))
                    {
                        if (!QueryExtensions.QueryImplementationType.Equals(type))
                        {
                            if (queryTypepredicate(type))
                            {
                                Console.WriteLine(type);
                                foreach (var queryCreator in new ChoiceInvocationContext(additionalChoiceInvocationFunc).GetConstructorInvocations(type))
                                {
                                    checkQueryGenericMethodInfo.MakeGenericMethod(@interface.GetGenericArguments())
                                        .Invoke(null, new object[] {connectionString, paramTypeChecker, queryResultPropertyTypeChecker, paramCreator, queryCreator});
                                }
                            }
                        }
                    }
                    if (typeof(INonQuery).Equals(@interface))
                    {
                        if (queryTypepredicate(type))
                        {
                            Console.WriteLine(type);
                            foreach (var queryCreator in new ChoiceInvocationContext(additionalChoiceInvocationFunc).GetConstructorInvocations(type))
                            {
                                checkNonQueriesGenericMethodInfo.Invoke(null, new object[] {connectionString, paramTypeChecker, paramCreator, queryCreator});
                            }
                        }
                    }
                }
            }
        }

        private static readonly MethodInfo checkNonQueriesGenericMethodInfo =
            Util.GetMethodInfo<Action<string, ParamTypeChecker, ParamCreator, QueryCreator>>((x1, x2, x3, x4) => CheckNonQuery(x1, x2, x3, x4));

        private static void CheckNonQuery(string connectionString, ParamTypeChecker paramTypeChecker, ParamCreator paramCreator, QueryCreator queryCreator)
        {
            CheckNonQueries(connectionString, paramTypeChecker, GetQueryFuncs<INonQuery>(queryCreator, paramCreator));
        }

        private static readonly MethodInfo checkQueryGenericMethodInfo =
            Util.GetMethodInfo<Action<string, ParamTypeChecker, QueryResultPropertyTypeChecker, ParamCreator, QueryCreator>>((x1, x2, x3, x4, x5) => CheckQuery<object>(x1, x2, x3, x4, x5)).GetGenericMethodDefinition();

        private static void CheckQuery<T>(string connectionString, ParamTypeChecker paramTypeChecker, QueryResultPropertyTypeChecker queryResultPropertyTypeChecker, ParamCreator paramCreator, QueryCreator queryCreator)
        {
            CheckQueries(connectionString, paramTypeChecker, queryResultPropertyTypeChecker, GetQueryFuncs<IQuery<T>>(queryCreator, paramCreator));
        }

        private static Func<CheckQueryContext, T> GetQueryFuncs<T>(QueryCreator queryCreator, ParamCreator paramCreator)
        {
            return queryContext => (T) queryCreator(
                new QueryCreatorContext(type => paramCreator(type, queryContext)));
        }

        private static IEnumerable<IEnumerable<TResult>> GetAllChoices<T, TResult>(
            IEnumerable<T> items,
            Func<T, IEnumerable<TResult>> choiceFunc)
        {
            return items.Aggregate(
                new IEnumerable<TResult>[] { },
                (seed, item) =>
                choiceFunc(item).SelectMany(
                    choiceItem => seed.Length == 0
                                          ? new[] { new[] { choiceItem } }
                                          : seed.Select(resultItem => resultItem.Concat(new[] { choiceItem }))
                    ).ToArray()
                );
        }

        private class ChoiceInvocationContext : IChoiceInvocationContext
        {
            private readonly Func<Type, IChoiceInvocationContext, IEnumerable<QueryCreator>> additionalChoiceInvocationFunc;

            internal ChoiceInvocationContext(
                Func<Type, IChoiceInvocationContext, IEnumerable<QueryCreator>> additionalChoiceInvocationFunc)
            {
                this.additionalChoiceInvocationFunc = additionalChoiceInvocationFunc;
            }

            public IEnumerable<QueryCreator> GetMethodBaseInvocations(MethodBase methodBase, Func<object[], object> invokeFunc)
            {
                var parameterInfos = methodBase.GetParameters();
                if (parameterInfos.Length == 0)
                {
                    return new QueryCreator[] { delegate { return invokeFunc(new object[] { }); } };
                }
                return GetAllChoices(parameterInfos, parameterInfo => GetChoiceInvocations(parameterInfo.ParameterType))
                    .Select<IEnumerable<QueryCreator>, QueryCreator>(
                        queryCreatorEnumerable => queryCreatorContext => invokeFunc(
                            queryCreatorEnumerable.Select(queryCreator => queryCreator(queryCreatorContext)).ToArray())
                    );
            }

            private IEnumerable<QueryCreator> GetChoiceInvocations(Type type)
            {
                if (type.IsGenericTypeAndEquals(typeof(IOptionalValue<>)))
                {
                    return new[] {asOptionalValueMethodInfo, nothingOptionalValueMethodInfo}.SelectMany(
                        genericMethodInfo => this.GetStaticMethodInvocations(genericMethodInfo.MakeGenericMethod(type.GetGenericArguments())));
                }
                else if (type.IsGenericTypeAndEquals(typeof(IParam<>)))
                {
                    return new QueryCreator[]
                           {
                               queryCreatorContext => queryCreatorContext.CreateParamFunc(type)
                           };
                }
                else if (typeof(bool).Equals(type))
                {
                    return new QueryCreator[] { delegate { return false; }, delegate { return true; } };
                }
                else if (type.IsEnum)
                {
                    return type.GetEnumValues().Cast<object>()
                        .Select<object, QueryCreator>(enumItem => delegate { return enumItem; });
                }
                else if (type.IsInterface)
                {
                    return GetImplementations(type).SelectMany(_ => this.GetConstructorInvocations(_));
                }
                else
                {
                    return additionalChoiceInvocationFunc(type, this);
                }
            }
        }

        private static readonly MethodInfo asOptionalValueMethodInfo = 
            Util.GetMethodInfo<Func<object, IOptionalValue<object>>>(_ => _.AsOptionalValue()).GetGenericMethodDefinition();

        private static readonly MethodInfo nothingOptionalValueMethodInfo =
            Util.GetMethodInfo<Func<IOptionalValue<object>>>(() => OptionalValue.Nothing<object>()).GetGenericMethodDefinition();

        private static IEnumerable<Type> GetImplementations(Type interfaceType)
        {
            foreach (var type in GetAllTypes())
            {
                foreach (var @interface in type.GetInterfaces())
                {
                    if (interfaceType.IsGenericType)
                    {
                        if (@interface.IsGenericType)
                        {
                            if (interfaceType.GetGenericTypeDefinition().Equals(@interface.GetGenericTypeDefinition()))
                            {
                                var typeArguments = type.GetGenericArguments();
                                var dictionary = typeArguments.Select((type1, index) => new { type = type1, index })
                                    .ToDictionary(arg => arg.type, arg => arg.index);
                                var interfaceArguments = @interface.GetGenericArguments();
                                var inputInterfaceArguments = interfaceType.GetGenericArguments();
                                for (var index = 0; index < interfaceArguments.Length; index++)
                                {
                                    typeArguments[dictionary[interfaceArguments[index]]] = inputInterfaceArguments[index];
                                }
                                yield return type.MakeGenericType(typeArguments);
                            }
                        }
                    }
                    else
                    {
                        if (interfaceType.Equals(@interface))
                        {
                            yield return type;
                        }
                    }
                }
            }
        }

        private static IEnumerable<Type> GetAllTypes()
        {
            return GetAllAssemblies().SelectMany(assembly => assembly.GetTypes());
        }

        private static IEnumerable<Assembly> GetAllAssemblies()
        {
            var assemblies = new HashSet<Assembly>();
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                assemblies.Add(assembly);
                LoadReferencedAssemblies(assembly, assemblies);
            }
            return assemblies;
        }

        private static void LoadReferencedAssemblies(Assembly assembly, HashSet<Assembly> assemblies)
        {
            foreach (var assemblyName in assembly.GetReferencedAssemblies())
            {
                var loadedAssembly = Assembly.Load(assemblyName.FullName);
                if (assemblies.Add(loadedAssembly))
                {
                    LoadReferencedAssemblies(loadedAssembly, assemblies);
                }
            }
        }
    }
}